﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;
using System.Windows.Media;
using System.Windows.Media.Media3D;

namespace GentlyWeep
{
    [ContentProperty("Children")]
    public class EllipseLayout3DPanel : FrameworkElement
    {
        private readonly Viewport3DVisual viewport3DVisual = new Viewport3DVisual();

        #region Constructor

        /// <summary>
        /// Static constructor which overrides default metadata for
        /// the UIElement.ClipToBoundsProperty, setting it to true.
        /// This ensures that the content of the control is not rendered
        /// if it is outside the bounds of the control.
        /// </summary>
        static EllipseLayout3DPanel()
        {
            ClipToBoundsProperty.OverrideMetadata(typeof(EllipseLayout3DPanel),
                                      new FrameworkPropertyMetadata(true,
                                            FrameworkPropertyMetadataOptions.Inherits));
        }

        /// <summary>
        /// Instantiates a new instance of the <see cref="EllipseLayout3DPanel"/> class.
        /// </summary>
        public EllipseLayout3DPanel()
        {
            ResourceDictionary localRD = new ResourceDictionary();
            //localRD.Source = new Uri("RDLocal.xaml", UriKind.Relative);
            localRD.Source = new Uri("RDLocal.xaml", UriKind.Relative);
            this.Resources.MergedDictionaries.Add(localRD);

            viewport3DVisual.Camera = TryFindResource("Camera") as Camera;
            viewport3DVisual.Children.Add(TryFindResource("AmbientLight") as ModelVisual3D);

            base.AddVisualChild(this.viewport3DVisual);
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets a collection of <see cref="UIElement"/> objects which define the control's content.
        /// This property is backed by the dependency property <see cref="ChildrenProperty"/>.
        /// </summary>
        public new ObservableCollection<UIElement> Children
        {
            get
            {
                ObservableCollection<UIElement> result = (ObservableCollection<UIElement>)base.GetValue(ChildrenProperty);

                if (result == null)
                {
                    result = new ObservableCollection<UIElement>();
                    //We want to know when new children are added so we
                    //can wrap them and send them on to the Viewport3DVisual.
                    result.CollectionChanged += Children_CollectionChanged;
                    base.SetValue(ChildrenProperty, result);
                }

                return result;
            }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="ChildrenProperty"/>.
        /// </summary>
        public static readonly DependencyProperty ChildrenProperty
                = DependencyProperty.Register("Children", typeof(ObservableCollection<UIElement>),
                                          typeof(EllipseLayout3DPanel), new FrameworkPropertyMetadata(null));

        /// <summary>
        /// Gets or sets the X coordinate of the layout ellipse's origin.
        /// This property is backed by the dependency property <see cref="EllipseCentreXProperty"/>.
        /// </summary>
        public double EllipseCentreX
        {
            get { return (double)GetValue(EllipseCentreXProperty); }
            set { SetValue(EllipseCentreXProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseCentreX"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseCentreXProperty
            = DependencyProperty.Register("EllipseCentreX", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the Y coordinate of the layout ellipse's origin.
        /// This property is backed by the dependency property <see cref="EllipseCentreYProperty"/>.
        /// </summary>
        public double EllipseCentreY
        {
            get { return (double)GetValue(EllipseCentreYProperty); }
            set { SetValue(EllipseCentreYProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseCentreY"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseCentreYProperty
            = DependencyProperty.Register("EllipseCentreY", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the Z coordinate of the layout ellipse's origin.
        /// This property is backed by the dependency property <see cref="EllipseCentreZProperty"/>.
        /// </summary>
        public double EllipseCentreZ
        {
            get { return (double)GetValue(EllipseCentreZProperty); }
            set { SetValue(EllipseCentreZProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseCentreZ"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseCentreZProperty
            = DependencyProperty.Register("EllipseCentreZ", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the height of the ellipse used to layout child content.
        /// This property is backed by the dependency property <see cref="EllipseHeightProperty"/>.
        /// </summary>
        public double EllipseHeight
        {
            get { return (double)GetValue(EllipseHeightProperty); }
            set { SetValue(EllipseHeightProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseHeight"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseHeightProperty
            = DependencyProperty.Register("EllipseHeight", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(300d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the width of the ellipse used to layout child content.
        /// This property is backed by the dependency property <see cref="EllipseWidthProperty"/>.
        /// </summary>
        public double EllipseWidth
        {
            get { return (double)GetValue(EllipseWidthProperty); }
            set { SetValue(EllipseWidthProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseWidth"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseWidthProperty
            = DependencyProperty.Register("EllipseWidth", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(300d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the angle of the layout ellipse's rotation about the x-axis.
        /// This property is backed by the dependency property <see cref="EllipseRotationXProperty"/>.
        /// </summary>
        public double EllipseRotationX
        {
            get { return (double)GetValue(EllipseRotationXProperty); }
            set { SetValue(EllipseRotationXProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseRotationX"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseRotationXProperty
            = DependencyProperty.Register("EllipseRotationX", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(98d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the angle of the layout ellipse's rotation about the y-axis.
        /// This property is backed by the dependency property <see cref="EllipseRotationYProperty"/>.
        /// </summary>
        public double EllipseRotationY
        {
            get { return (double)GetValue(EllipseRotationYProperty); }
            set { SetValue(EllipseRotationYProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseRotationY"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseRotationYProperty
            = DependencyProperty.Register("EllipseRotationY", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsArrange));

        /// <summary>
        /// Gets or sets the angle of the layout ellipse's rotation about the x-axis.
        /// This property is backed by the dependency property <see cref="EllipseRotationZProperty"/>.
        /// </summary>
        public double EllipseRotationZ
        {
            get { return (double)GetValue(EllipseRotationZProperty); }
            set { SetValue(EllipseRotationZProperty, value); }
        }

        /// <summary>
        /// Dependency property backing store for <see cref="EllipseRotationZ"/>.
        /// Changes to this property cause the control to arrange itself, indicated with
        /// the <see cref="FrameworkPropertyMetadataOptions"/>.AffectsArrange value.
        /// </summary>
        public static readonly DependencyProperty EllipseRotationZProperty
            = DependencyProperty.Register("EllipseRotationZ", typeof(double), typeof(EllipseLayout3DPanel),
                                          new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.AffectsArrange));

        #endregion

        #region Arrange / Measure Overrides

        protected override Size ArrangeOverride(Size finalSize)
        {
            Rect rect = new Rect(new Point(), finalSize);

            this.viewport3DVisual.Viewport = rect;

            //Work out the circumference of our Ellipse as this may have changed.
            double layoutCirfumference
                = CalculateEllipseCircumferenceRamanujan2(EllipseWidth, EllipseHeight);

            //Calculate the spacing between each item.
            //Subtract one off the child count to account for the light object.
            double itemSpacing = layoutCirfumference / (viewport3DVisual.Children.Count - 1);

            //Iterate the children. We could have used a foreach, but
            //as we need to know the index of the child we are working with,
            //this is cleaner.
            for (int i = 0; i < viewport3DVisual.Children.Count; i++)
            {
                //We want the item as a FrameworkElement so that we can
                //get its height and width.
                Viewport2DVisual3D child = viewport3DVisual.Children[i] as Viewport2DVisual3D;

                if (child == null)
                {
                    continue;
                }

                //Work out how far around the ellipse we are
                double theta = (Math.PI * 2) * ((i * itemSpacing) / layoutCirfumference);
                //Offset so the ellipse starts at the top.
                theta -= Math.PI / 2;

                //Now to calculate the point on the edge of the ellipse for the current rotation.
                Point p = GetPointOnEllipse(EllipseWidth,
                                            EllipseHeight,
                                            new Point(EllipseCentreX,
                                                      EllipseCentreY),
                                            theta);

                //We now have the x and the y values of the target point on an ellipse in 2D. 
                //We need to rotate this position by the same amount that the ellipse has been
                //rotated so that we get the correct 3D position on the ellipse
                Point3D rotatedPoint = RotatePoint3D(new Point3D(p.X, p.Y, 0d), 
                                                     EllipseRotationX, 
                                                     EllipseRotationY, 
                                                     EllipseRotationZ);

                //Create a 3D translation transformation so that the button appears in the correct
                //place.
                TranslateTransform3D translate
                    = new TranslateTransform3D(rotatedPoint.X, rotatedPoint.Y, rotatedPoint.Z);

                //Add the new translation in to the transform group.
                AssertTransform3D<TranslateTransform3D>(translate, child);
            }

            return finalSize;
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            //Iterate all the children of the inner viewport.
            foreach (Viewport2DVisual3D visualChild in viewport3DVisual.Children.OfType<Viewport2DVisual3D>())
            {
                //Get the inner UIElement
                UIElement element = visualChild.Visual as UIElement;

                //Create a scale transform so that the control appears the right size
                ScaleTransform3D scaleTransform 
                    = new ScaleTransform3D(element.DesiredSize.Width, element.DesiredSize.Height, 0d);

                //Add the scale transform in to the Viewport2DVisual3D's transform group.
                AssertTransform3D<ScaleTransform3D>(scaleTransform, visualChild);
            }
            
            return availableSize;
        }

        #endregion

        #region FrameworkElement Overrides

        /* 
         * These next three overrides handle the fact that we're wrapping
         * a Viewport3DVisual which is the single logical child of this
         * ellipse control.
         */

        private static object ChildrenGetValueOverride(DependencyObject d)
        {
            return ((EllipseLayout3DPanel)d).viewport3DVisual.Children;
        }

        protected override Visual GetVisualChild(int index)
        {
            if (index != 0)
            {
                throw new ArgumentOutOfRangeException("index", index, "Visual_ArgumentOutOfRange");
            }

            return this.viewport3DVisual;
        }

        protected override int VisualChildrenCount
        {
            get { return 1; }
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Small helper method to wrap a Visual in a Viewport2DVisual3D.
        /// </summary>
        /// <param name="wrapMe"></param>
        /// <returns></returns>
        private Viewport2DVisual3D WrapUIElement(Visual wrapMe)
        {
            Viewport2DVisual3D result = new Viewport2DVisual3D();

            result.Geometry = TryFindResource("ViewportControlMesh") as Geometry3D;
            result.Material = TryFindResource("ViewportControlMaterial") as Material;
            result.Visual = wrapMe;

            return result;
        }

        /// <summary>
        /// This method is invoked when the CollectionChanged event is raised on our Children
        /// property. We need to look at whether items have been added or removed from the
        /// control and update the inner Viewport3DVisual accordingly.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void Children_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
        {
            //Look at how the collection has changed.
            switch (e.Action)
            {
                case NotifyCollectionChangedAction.Add:

                    //For each of the new child controls added, we need to
                    //wrap it in a Viewport2DVisual3D and add it to the inner
                    //Viewport3DVisual.
                    foreach (UIElement newItem in e.NewItems)
                    {
                        viewport3DVisual.Children.Add(WrapUIElement(newItem));
                    }
                    break;

                case NotifyCollectionChangedAction.Remove:

                    //For each of the child objects that have been removed, we
                    //need to also remove them from the inner Viewport3DVisual.
                    List<Viewport2DVisual3D> itemsToRemove = 
                        (from child in viewport3DVisual.Children.OfType<Viewport2DVisual3D>()
                         where e.OldItems.Contains(child.Visual)
                         select child).ToList<Viewport2DVisual3D>();

                    //Iterate the items we want to remove and remove them.
                    foreach (Viewport2DVisual3D visual in itemsToRemove)
                    {
                        //Clear the Visual property so that the inner UIElement will
                        //be removed from the visual tree.
                        visual.Visual = null;
                        viewport3DVisual.Children.Remove(visual);
                    }                    
                    break;
            }

            //Invalidate the arrangement and measurement of the control so
            //that on the next pass the positioning of the child controls
            //are updated to reflect the changes.
            InvalidateMeasure();
            InvalidateArrange();
        }

        #endregion

        #region Private Static Methods
        /*
         * These methods are static to indicate that they do not need to be contained within
         * the control, and most like would be found in a helper class of some kind.
         */

        /// <summary>
        ///   <para>
        ///     This method is rather cool in that it allows you to set / update a transform on a Visual3D
        ///     without having to worry about affecting existing transformations if a
        ///     Transform3DGroup is in use. 
        ///   </para>
        ///   <para>
        ///     If the <paramref name="visual3D.Transform"/> property is null,
        ///     a new <see cref="Transform3DGroup"/> will be created and <paramref name="transform"/> will
        ///     be added in to it.
        ///   </para>
        ///   <para>
        ///     If the <paramref name="visual3D.Transform"/> property is not null, and contains a transform
        ///     of type <typeparamref name="T"/>, the transform will be replaced, otherwise it will be added.
        ///   </para>
        /// </summary>
        /// <typeparam name="T">The <see cref="Type"/> of the 3D transform class we are asserting.</typeparam>
        /// <param name="transform">The instance of the transform we are asserting.</param>
        /// <param name="visual3D">The <see cref="Visual3D"/> we want the transform to be applied to.</param>
        private static void AssertTransform3D<T>(T transform, Visual3D visual3D) where T : Transform3D
        {            
            Transform3DGroup transform3DGroup = visual3D.Transform as Transform3DGroup;

            if (transform3DGroup == null)
            {
                transform3DGroup = new Transform3DGroup();
                visual3D.Transform = transform3DGroup;
            }

            //Given the way we are handling transforms on objects, there will only
            //be either a scale transform and or a translate transform, or neither.
            Transform3D target = (from transform3D in transform3DGroup.Children
                                  where transform3D is T
                                  select transform3D).FirstOrDefault<Transform3D>();
            
            //If we didn't find the target transform, add it in.
            if (target == null)
            {
                transform3DGroup.Children.Add(transform);
            }
            else
            {
                //Out with old and in with the new...
                transform3DGroup.Children.Remove(target);
                transform3DGroup.Children.Add(transform);
            }
        }

        /// <summary>
        /// Calculates the point on an ellipse at the given angle.
        /// </summary>        
        /// <param name="width">The width of the ellipse (length of major axis).</param>
        /// <param name="height">The height of the ellipse (length of minor axis).</param>
        /// <param name="origin">The location of the ellipse's centre.</param>
        /// <param name="thea">The angle (in radians) at which the point of interest lies.</param>
        /// <returns>A <see cref="Point"/> object detailing the coordinates of the point on the ellipse.</returns>
        private static Point GetPointOnEllipse(double width, double height, Point origin, double theta)
        {
            //This code uses the rather expensive Math.Cos() and Math.Sin() methods.
            //If performance is a big issue you may want to consider using a look
            //up table of precomputed angles.
            double x = origin.X + (width * Math.Cos(theta));
            double y = origin.Y + (height * Math.Sin(theta));

            return new Point(x, y);
        }

        /// <summary>
        /// Uses the second Ramanujan approximation to calculate the circumference of an ellipse.
        /// </summary>
        /// <returns>The circumference of an ellipse with a width <param name="a"/> and height <param name="b" />.</returns>
        private static double CalculateEllipseCircumferenceRamanujan2(double a, double b)
        {
            double h = Math.Pow(a - b, 2) / Math.Pow(a + b, 2);

            //pi (a + b) [ 1 + 3 h / (10 + (4 - 3 h)^1/2 ) ]            
            double result = Math.PI * (a + b) * (1 + 3 * h / (10 + Math.Pow((4 - 3 * h), 0.5)));

            return result;
        }

        //Create these here so we don't have to create them each time.
        private readonly static Vector3D UnitXAxis3D = new Vector3D(1d, 0d, 0d);
        private readonly static Vector3D UnitYAxis3D = new Vector3D(0d, 1d, 0d);
        private readonly static Vector3D UnitZAxis3D = new Vector3D(0d, 0d, 1d);

        /// <summary>
        /// Rotates a <paramref name="point"/> by the given angles for each axis.
        /// </summary>
        /// <param name="point">The point to rotate.</param>
        /// <param name="xRotation">The angle by which to rotate <paramref name="point"/> around the x-axis.</param>
        /// <param name="yRotation">The angle by which to rotate <paramref name="point"/> around the y-axis.</param>
        /// <param name="zRotation">The angle by which to rotate <paramref name="point"/> around the z-axis.</param>
        /// <returns>The original point in its rotated form.</returns>
        private static Point3D RotatePoint3D(Point3D point, double xRotation, double yRotation, double zRotation)
        {
            //Refer to http://www.genesis3d.com/~kdtop/Quaternions-UsingToRepresentRotation.htm for more
            //information on quaternions

            //Use quaternions to avoid Gimbal lock.
            Quaternion xQ = new Quaternion(UnitXAxis3D, xRotation);
            Quaternion yQ = new Quaternion(UnitYAxis3D, yRotation);
            Quaternion zQ = new Quaternion(UnitZAxis3D, zRotation);

            //Combine the above three quaternions.
            Quaternion xyzQ = xQ * yQ * zQ;
            //Now take a copy of this quaternion and calculate its conjugate.
            Quaternion xyzQc = xyzQ;
            xyzQc.Conjugate();

            //Get out point as a Quaternion.
            Quaternion pQ = new Quaternion(point.X, point.Y, point.Z, 0d);

            //Now multiply everything together
            Quaternion q = xyzQ * pQ * xyzQc;

            //And extract our point.
            Point3D rotatedPoint = new Point3D(q.X, q.Y, q.Z);

            return rotatedPoint;
        }

        #endregion
    }
}